﻿using System;
using System.Collections.Generic;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Xml;

namespace RockBus.ServiceModel
{
    public static class OperationContextExtensions
    {
        public static void CopyMessageContext(this OperationContext operationContext)
        {
            //Guid messageId;
            //if ((null != operationContext.IncomingMessageHeaders.MessageId) && operationContext.IncomingMessageHeaders.MessageId.TryGetGuid(out messageId))
            //{
            //    operationContext.IncomingMessageProperties.SetMessageId(messageId);
            //}
            //else
            //{
            //    operationContext.IncomingMessageProperties.SetMessageId(Guid.NewGuid());
            //}

            RemoteEndpointMessageProperty endpointProperty = null;
            if (operationContext.IncomingMessageProperties.ContainsKey(RemoteEndpointMessageProperty.Name))
            {
                object endpointPropertyObj = operationContext.IncomingMessageProperties[RemoteEndpointMessageProperty.Name];
                if (null != endpointPropertyObj)
                {
                    endpointProperty = endpointPropertyObj as RemoteEndpointMessageProperty;
                    if (null != endpointProperty)
                    {
                        operationContext.IncomingMessageProperties.SetReceiveAddress(endpointProperty.Address);
                    }
                }
            }

            if (null == endpointProperty)
            {
                operationContext.IncomingMessageProperties.SetReceiveAddress(operationContext.EndpointDispatcher.EndpointAddress.Uri.ToString());
            }
        }
    }

    public static class MessageExtensions
    {
        private const string MessageIdKey = "RockBus.MessageId";

        public static Guid? GetMessageId(this MessageProperties messageProperties)
        {
            Guid value;
            if (messageProperties.TryGetProperty(MessageIdKey, out value))
            {
                return value;
            }

            return null;
        }

        public static void SetMessageId(this MessageProperties messageProperties, Guid messageId)
        {
            messageProperties[MessageIdKey] = messageId;
        }

        private const string ReceiveAddressKey = "RockBus.ReceiveAddressKey";

        public static string GetReceiveAddress(this MessageProperties messageProperties)
        {
            string value;
            messageProperties.TryGetProperty(ReceiveAddressKey, out value);
            return value;
        }

        public static void SetReceiveAddress(this MessageProperties messageProperties, string receiveAddress)
        {
            messageProperties[ReceiveAddressKey] = receiveAddress;
        }

        private const string TopicHeaderKey = "RockBus.TopicHeaderKey";

        public static object GetTopicHeader(this MessageProperties messageProperties)
        {
            object value;
            messageProperties.TryGetProperty(TopicHeaderKey, out value);
            return value;
        }

        public static void SetTopicHeader(this MessageProperties messageProperties, object topicHeader)
        {
            messageProperties[TopicHeaderKey] = topicHeader;
        }

        private const string PublisherReceivePortInfoKey = "RockBus.PublisherReceivePortInfoKey";

        public static object GetPublisherReceivePortInfo(this MessageProperties messageProperties)
        {
            object value;
            messageProperties.TryGetProperty(PublisherReceivePortInfoKey, out value);
            return value;
        }

        public static void SetPublisherReceivePortInfo(this MessageProperties messageProperties, object receivePortInfo)
        {
            messageProperties[PublisherReceivePortInfoKey] = receivePortInfo;
        }

        public static bool TryGetProperty<T>(this MessageProperties messageProperties, string key, out T value)
        //where T : class
        {
            value = default(T);
            if (messageProperties.ContainsKey(key))
            {
                value = (T)messageProperties[key];
            }

            return (value != null);
        }

        public static T FindHeader<T>(this MessageHeaders messageHeaders, string headerName, string headerNamespace, bool remove = false)
            where T : class
        {
            T header = null;
            int messageHeaderIndex = messageHeaders.FindHeader(headerName, headerNamespace);
            if (messageHeaderIndex >= 0)
            {
                header = messageHeaders.GetHeader<T>(messageHeaderIndex);
                if (remove)
                {
                    messageHeaders.RemoveAt(messageHeaderIndex);
                }
            }
            return header;
        }

        public static Message MarshalMessageCloseSource(this Message source, Uri to, MessageVersion targetVersion)
        {
            Message marshalledMessage = null;
            try
            {
                marshalledMessage = source.MarshalMessage(to, targetVersion);
                return marshalledMessage;
            }
            finally
            {
                // Test for reference equality
                //  If a new message was created by MarshalMessage, close the original (which we created just before)
                if (source != marshalledMessage)
                {
                    source.Close();
                }
            }
        }

        public static Message MarshalMessage(this Message source, Uri to, MessageVersion targetVersion)
        {
            Message message;
            MessageHeaders headers = source.Headers;
            MessageVersion version = source.Version;
            UnderstoodHeaders understoodHeaders = headers.UnderstoodHeaders;
            HashSet<string> understoodHeadersSet = CreateKeys(understoodHeaders);
            if ((version == targetVersion) && !RoutingUtilities.IsMessageUsingWSSecurity(understoodHeaders))
            {
                FilterHeaders(headers, understoodHeadersSet);
                FilterProperties(source.Properties);
                return source;
            }
            if (source.IsFault)
            {
                MessageFault fault = MessageFault.CreateFault(source, 0x7fffffff);
                string action = headers.Action;
                //if (string.Equals(action, version.Addressing.DefaultFaultAction, StringComparison.Ordinal))
                //{
                //    action = targetVersion.Addressing.DefaultFaultAction;
                //}
                message = Message.CreateMessage(targetVersion, fault, action);
            }
            else if (source.IsEmpty)
            {
                message = Message.CreateMessage(targetVersion, headers.Action);
            }
            else
            {
                XmlDictionaryReader readerAtBodyContents = source.GetReaderAtBodyContents();
                message = Message.CreateMessage(targetVersion, headers.Action, readerAtBodyContents);
            }
            CloneHeaders(message.Headers, headers, to, understoodHeadersSet);
            CloneProperties(message.Properties, source.Properties);
            return message;
        }

        private static HashSet<string> CreateKeys(UnderstoodHeaders headers)
        {
            HashSet<string> set = new HashSet<string>();
            foreach (MessageHeaderInfo info in headers)
            {
                set.Add(MessageHeaderKey(info));
            }
            return set;
        }

        private static string MessageHeaderKey(MessageHeaderInfo header)
        {
            return (header.Name + "+" + header.Namespace);
        }

        private static void FilterHeaders(MessageHeaders headers, HashSet<string> understoodHeadersSet)
        {
            string addressingNamespace = RoutingUtilities.GetAddressingNamespace(headers.MessageVersion.Addressing);
            for (int i = headers.Count - 1; i >= 0; i--)
            {
                MessageHeaderInfo header = headers[i];
                bool flag = false;
                if (!string.Equals(header.Namespace, addressingNamespace, StringComparison.Ordinal) || (!addressingHeadersToFlow.Contains(header.Name) && !manualAddressing))
                {
                    if (understoodHeadersSet.Contains(MessageHeaderKey(header)))
                    {
                        flag = true;
                    }
                    else if (ActorIsNextDestination(header, headers.MessageVersion) && !header.Relay)
                    {
                        flag = true;
                    }
                    if (flag)
                    {
                        headers.RemoveAt(i);
                    }
                }
            }
        }

        private static bool ActorIsNextDestination(MessageHeaderInfo header, MessageVersion messageVersion)
        {
            return ((header.Actor != null) && string.Equals(header.Actor, messageVersion.Envelope.NextDestinationActorValue));
        }

        private static void FilterProperties(MessageProperties destination)
        {
            object obj2;
            if (destination.TryGetValue(HttpRequestMessageProperty.Name, out obj2))
            {
                destination["IncomingHttpRequest"] = obj2;
                destination.Remove(HttpRequestMessageProperty.Name);
            }
            destination.Remove(HttpResponseMessageProperty.Name);
            Uri via = destination.Via;
            if (via != null)
            {
                destination["IncomingVia"] = via;
            }
        }

        private const bool manualAddressing = false;

        private static void CloneHeaders(MessageHeaders targetHeaders, MessageHeaders sourceHeaders, Uri to, HashSet<string> understoodHeadersSet)
        {
            for (int i = 0; i < sourceHeaders.Count; i++)
            {
                MessageHeaderInfo info = sourceHeaders[i];
                if (!understoodHeadersSet.Contains(MessageHeaderKey(info)) && (!ActorIsNextDestination(info, sourceHeaders.MessageVersion) || info.Relay))
                {
                    MessageHeader header = new DelegatingHeader(info, sourceHeaders);
                    targetHeaders.Add(header);
                }
            }
            targetHeaders.To = to;
            if (targetHeaders.MessageVersion.Addressing != AddressingVersion.None)
            {
                targetHeaders.MessageId = sourceHeaders.MessageId;
                targetHeaders.RelatesTo = sourceHeaders.RelatesTo;

                //if (manualAddressing)
                //{
                //    targetHeaders.FaultTo = sourceHeaders.FaultTo;
                //    targetHeaders.ReplyTo = sourceHeaders.ReplyTo;
                //    targetHeaders.From = sourceHeaders.From;
                //}
            }
        }

        private static void CloneProperties(MessageProperties destination, MessageProperties source)
        {
            MessageEncoder encoder = destination.Encoder;
            destination.CopyProperties(source);
            destination.Encoder = encoder;
            FilterProperties(destination);
        }

        private readonly static HashSet<string> addressingHeadersToFlow = InitializeHeadersToFlow();

        private static HashSet<string> InitializeHeadersToFlow()
        {
            HashSet<string> set = new HashSet<string>(StringComparer.Ordinal);
            set.Add("Action");
            set.Add("MessageID");
            set.Add("RelatesTo");
            set.Add("To");
            return set;
        }

        /// <summary>
        /// N.B. Consumes the message!
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public static string MessageAsString(this Message message)
        {
            try
            {
                var sb = new StringBuilder();
                using (var xw = XmlWriter.Create(sb, new XmlWriterSettings { Encoding = new UTF8Encoding(), OmitXmlDeclaration = true }))
                {
                    message.WriteMessage(xw);
                    xw.Close();
                }
                return sb.ToString();
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }
    }
}